<!DOCTYPE html>
<html>
    <head>
        <meta name="description" content="HTML5 Audio Spectrum Visualizer">
        <title>HTML5 Audio API showcase | Audio visualizer</title>
        <style type="text/css">
            html, body {
                margin: 0;
                font-family: arial, "Microsoft YaHei";
                background-color: #272822;
                color: #FEFEFE;
            }
            #fileWrapper{
                transition:all 0.5s ease;
            }
            #fileWrapper:hover{
                opacity: 1!important;
            }
            #visualizer_wrapper{
                text-align: center;
            }
            footer{
                position: fixed;
                bottom: 2px;
                color:#aaa;
            }
        </style>
    </head>
    <body>
        <div id="wrapper">
            <div id="fileWrapper" class="file_wrapper">
                <div id="info">
                    HTML5 Audio API showcase | An Audio Viusalizer
                </div>
                <label for="uploadedFile">Drag&drop or select a file to play:</label>
                <input type="file" id="uploadedFile"></input>
            </div>
            <div id="visualizer_wrapper">
                <canvas id='canvas' width="800" height="350"></canvas>
            </div>
        </div>
        <footer>
            <small>Star me on <a href="https://github.com/Wayou/HTML5_Audio_Visualizer" target="_blank">GitHub</a></small>
        </footer>
        <script type="text/javascript">
            /*
             * An audio spectrum visualizer built with HTML5 Audio API
             * Author:Wayou
             * License: MIT
             * Feb 15, 2014
             */
            window.onload = function() {
                new Visualizer().ini();
            };
            var Visualizer = function() {
                this.file = null, //the current file
                this.fileName = null, //the current file name
                this.audioContext = null,
                this.source = null, //the audio source
                this.info = document.getElementById('info').innerHTML, //this used to upgrade the UI information
                this.infoUpdateId = null, //to sotore the setTimeout ID and clear the interval
                this.animationId = null,
                this.status = 0, //flag for sound is playing 1 or stopped 0
                this.forceStop = false,
                this.allCapsReachBottom = false
            };
            Visualizer.prototype = {
                ini: function() {
                    this._prepareAPI();
                    this._addEventListner();
                },
                _prepareAPI: function() {
                    //fix browser vender for AudioContext and requestAnimationFrame
                    window.AudioContext = window.AudioContext || window.webkitAudioContext || window.mozAudioContext || window.msAudioContext;
                    window.requestAnimationFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.msRequestAnimationFrame;
                    window.cancelAnimationFrame = window.cancelAnimationFrame || window.webkitCancelAnimationFrame || window.mozCancelAnimationFrame || window.msCancelAnimationFrame;
                    try {
                        this.audioContext = new AudioContext();
                    } catch (e) {
                        this._updateInfo('!Your browser does not support AudioContext', false);
                        console.log(e);
                    }
                },
                _addEventListner: function() {
                    var that = this,
                        audioInput = document.getElementById('uploadedFile'),
                        dropContainer = document.getElementsByTagName("canvas")[0];
                    //listen the file upload
                    audioInput.onchange = function() {
                        if (that.audioContext===null) {return;};

                        //the if statement fixes the file selction cancle, because the onchange will trigger even the file selection been canceled
                        if (audioInput.files.length !== 0) {
                            //only process the first file
                            that.file = audioInput.files[0];
                            that.fileName = that.file.name;
                            if (that.status === 1) {
                                //the sound is still playing but we upload another file, so set the forceStop flag to true
                                that.forceStop = true;
                            };
                            document.getElementById('fileWrapper').style.opacity = 1;
                            that._updateInfo('Uploading', true);
                            //once the file is ready,start the visualizer
                            that._start();
                        };
                    };
                    //listen the drag & drop
                    dropContainer.addEventListener("dragenter", function() {
                        document.getElementById('fileWrapper').style.opacity = 1;
                        that._updateInfo('Drop it on the page', true);
                    }, false);
                    dropContainer.addEventListener("dragover", function(e) {
                        e.stopPropagation();
                        e.preventDefault();
                        //set the drop mode
                        e.dataTransfer.dropEffect = 'copy';
                    }, false);
                    dropContainer.addEventListener("dragleave", function() {
                        document.getElementById('fileWrapper').style.opacity = 0.2;
                        that._updateInfo(that.info, false);
                    }, false);
                    dropContainer.addEventListener("drop", function(e) {
                        e.stopPropagation();
                        e.preventDefault();
                        if (that.audioContext===null) {return;};
                        document.getElementById('fileWrapper').style.opacity = 1;
                        that._updateInfo('Uploading', true);
                        //get the dropped file
                        that.file = e.dataTransfer.files[0];
                        if (that.status === 1) {
                            document.getElementById('fileWrapper').style.opacity = 1;
                            that.forceStop = true;
                        };
                        that.fileName = that.file.name;
                        //once the file is ready,start the visualizer
                        that._start();
                    }, false);
                },
                _start: function() {
                    //read and decode the file into audio array buffer
                    var that = this,
                        file = this.file,
                        fr = new FileReader();
                    fr.onload = function(e) {
                        var fileResult = e.target.result;
                        var audioContext = that.audioContext;
                        if (audioContext === null) {
                            return;
                        };
                        that._updateInfo('Decoding the audio', true);
                        audioContext.decodeAudioData(fileResult, function(buffer) {
                            that._updateInfo('Decode succussfully,start the visualizer', true);
                            that._visualize(audioContext, buffer);
                        }, function(e) {
                            that._updateInfo('!Fail to decode the file', false);
                            console.log(e);
                        });
                    };
                    fr.onerror = function(e) {
                        that._updateInfo('!Fail to read the file', false);
                        console.log(e);
                    };
                    //assign the file to the reader
                    this._updateInfo('Starting read the file', true);
                    fr.readAsArrayBuffer(file);
                },
                _visualize: function(audioContext, buffer) {
                    var audioBufferSouceNode = audioContext.createBufferSource(),
                        analyser = audioContext.createAnalyser(),
                        that = this;
                    //connect the source to the analyser
                    audioBufferSouceNode.connect(analyser);
                    //connect the analyser to the destination(the speaker), or we won't hear the sound
                    analyser.connect(audioContext.destination);
                    //then assign the buffer to the buffer source node
                    audioBufferSouceNode.buffer = buffer;
                    //play the source
                    if (!audioBufferSouceNode.start) {
                        audioBufferSouceNode.start = audioBufferSouceNode.noteOn //in old browsers use noteOn method
                        audioBufferSouceNode.stop = audioBufferSouceNode.noteOff //in old browsers use noteOn method
                    };
                    //stop the previous sound if any
                    if (this.animationId !== null) {
                        cancelAnimationFrame(this.animationId);
                    }
                    if (this.source !== null) {
                        this.source.stop(0);
                    }
                    audioBufferSouceNode.start(0);
                    this.status = 1;
                    this.source = audioBufferSouceNode;
                    audioBufferSouceNode.onended = function() {
                        that._audioEnd(that);
                    };
                    this._updateInfo('Playing ' + this.fileName, false);
                    this.info = 'Playing ' + this.fileName;
                    document.getElementById('fileWrapper').style.opacity = 0.2;
                    this._drawSpectrum(analyser);
                },
                _drawSpectrum: function(analyser) {
                    var that = this,
                        canvas = document.getElementById('canvas'),
                        cwidth = canvas.width,
                        cheight = canvas.height - 2,
                        meterWidth = 10, //width of the meters in the spectrum
                        gap = 2, //gap between meters
                        capHeight = 2,
                        capStyle = '#fff',
                        meterNum = 800 / (10 + 2), //count of the meters
                        capYPositionArray = []; ////store the vertical position of hte caps for the preivous frame
                    ctx = canvas.getContext('2d'),
                    gradient = ctx.createLinearGradient(0, 0, 0, 300);
                    gradient.addColorStop(1, '#0f0');
                    gradient.addColorStop(0.5, '#ff0');
                    gradient.addColorStop(0, '#f00');
                    var drawMeter = function() {
                        var array = new Uint8Array(analyser.frequencyBinCount);
                        analyser.getByteFrequencyData(array);
                        if (that.status === 0) {
                            //fix when some sounds end the value still not back to zero
                            for (var i = array.length - 1; i >= 0; i--) {
                                array[i] = 0;
                            };
                            allCapsReachBottom = true;
                            for (var i = capYPositionArray.length - 1; i >= 0; i--) {
                                allCapsReachBottom = allCapsReachBottom && (capYPositionArray[i] === 0);
                            };
                            if (allCapsReachBottom) {
                                cancelAnimationFrame(that.animationId); //since the sound is top and animation finished, stop the requestAnimation to prevent potential memory leak,THIS IS VERY IMPORTANT!
                                return;
                            };
                        };
                        var step = Math.round(array.length / meterNum); //sample limited data from the total array
                        ctx.clearRect(0, 0, cwidth, cheight);
                        for (var i = 0; i < meterNum; i++) {
                            var value = array[i * step];
                            if (capYPositionArray.length < Math.round(meterNum)) {
                                capYPositionArray.push(value);
                            };
                            ctx.fillStyle = capStyle;
                            //draw the cap, with transition effect
                            if (value < capYPositionArray[i]) {
                                ctx.fillRect(i * 12, cheight - (--capYPositionArray[i]), meterWidth, capHeight);
                            } else {
                                ctx.fillRect(i * 12, cheight - value, meterWidth, capHeight);
                                capYPositionArray[i] = value;
                            };
                            ctx.fillStyle = gradient; //set the filllStyle to gradient for a better look
                            ctx.fillRect(i * 12 /*meterWidth+gap*/ , cheight - value + capHeight, meterWidth, cheight); //the meter
                        }
                        that.animationId = requestAnimationFrame(drawMeter);
                    }
                    this.animationId = requestAnimationFrame(drawMeter);
                },
                _audioEnd: function(instance) {
                    if (this.forceStop) {
                        this.forceStop = false;
                        this.status = 1;
                        return;
                    };
                    this.status = 0;
                    var text = 'HTML5 Audio API showcase | An Audio Viusalizer';
                    document.getElementById('fileWrapper').style.opacity = 1;
                    document.getElementById('info').innerHTML = text;
                    instance.info = text;
                    document.getElementById('uploadedFile').value = '';
                },
                _updateInfo: function(text, processing) {
                    var infoBar = document.getElementById('info'),
                        dots = '...',
                        i = 0,
                        that = this;
                    infoBar.innerHTML = text + dots.substring(0, i++);
                    if (this.infoUpdateId !== null) {
                        clearTimeout(this.infoUpdateId);
                    };
                    if (processing) {
                        //animate dots at the end of the info text
                        var animateDot = function() {
                            if (i > 3) {
                                i = 0
                            };
                            infoBar.innerHTML = text + dots.substring(0, i++);
                            that.infoUpdateId = setTimeout(animateDot, 250);
                        }
                        this.infoUpdateId = setTimeout(animateDot, 250);
                    };
                }
            }
        </script>
        <script>
            (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
                (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
                m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
            })(window,document,'script','//www.google-analytics.com/analytics.js','ga');
            ga('create', 'UA-46794744-3', 'duapp.com');
            ga('send', 'pageview');
        </script>
    </body>
</html>